Лабораторная работа 8 "Блок загрузки и сохранения"
Итогом шестой лабораторной работы стал практически завершенный процессор архитектуры RISC-V. Особенностью той реализации процессора было отсутствие поддержки инструкций LB
, LBU
, SB
, LH
, LHU
, SH
. Тому было две причины:
- в третьей лабораторной работе была реализована память данных, не поддерживавшая возможность обновления отдельных байт;
- необходимо подготовить считанные из памяти полуслова / байты для записи в регистровый файл.
В седьмой лабораторной работе была реализована новая память данных, в которой можно управлять записью в отдельные байты посредством управляющего сигнала byte_enable
, каждый отдельный бит которого является разрешением записи в соответствующий байт слова. Необходимо управлять этим сигналом, используя интерфейс памяти процессора.
Для этих целей используется специальный модуль — Блок загрузки и сохранения (Load and Store Unit, LSU).
Цель
Разработка блока загрузки и сохранения для подключения к внешней памяти данных, поддерживающей запись в отдельные байты памяти.
Ход работы
Изучить:
- Функции и задачи блока загрузки/сохранения
- Интерфейс процессора и блока загрузки/сохранения
- Интерфейс блока загрузки/сохранения и памяти
Реализовать и проверить модуль riscv_lsu
.
Теория
Модуль загрузки и сохранения (Load/Store Unit – LSU) служит для исполнения инструкций типа LOAD
и STORE
: является прослойкой между внешним устройством – памятью, и ядром процессора. LSU считывает содержимое из памяти данных или записывает в нее требуемые значения, преобразуя 8- и 16-битные данные в знаковые или беззнаковые 32-битные числа для регистров процессора. В процессорах с RISC архитектурой с помощью LSU осуществляется обмен данными между регистрами общего назначения и памятью данных.
Рисунок 1. Место LSU в микроархитектуре RISC-процессора.
Интерфейс процессора и блока загрузки/сохранения
Параграф посвящен описанию сигналов и правил взаимодействия между процессором и блоком загрузки/сохранения LSU (core protocol).
На входной порт core_addr_i
от процессора поступает адрес ячейки памяти, к которой будет произведено обращение. Намеренье процессора обратиться к памяти (и для чтения, и для записи) отражается выставлением сигнала core_req_i
в единицу. Если процессор собирается записывать в память, то сигнал core_we_i
выставляется в единицу, а сами данные, которые следует записать, поступают от него на вход core_wd_i
. Если процессор собирается читать из памяти, то сигнал core_we_i
находится в нуле, а считанные данные подаются для процессора на выход core_rd_o
.
Инструкции LOAD
и STORE
в RV32I поддерживают обмен 8-битными, 16-битными или 32-битными значениями, однако в самом процессоре происходит работа только с 32-битными числами, поэтому загружая байты или полуслова из памяти их необходимо предварительно расширить до 32-битного значения. Для выбора разрядности на вход LSU подается сигнал core_size_i
, принимающий следующие значения:
Название | Значение | Пояснение |
---|---|---|
LDST_B | 3'd0 | Знаковое 8-битное значение |
LDST_H | 3'd1 | Знаковое 16-битное значение |
LDST_W | 3'd2 | 32-битное значение |
LDST_BU | 3'd4 | Беззнаковое 8-битное значение |
LDST_HU | 3'd5 | Беззнаковое 16-битное значение |
Формат представления числа (является оно знаковым или беззнаковым) имеет значение только для операций типа LOAD
: если число знаковое, то производится расширение знака до 32 бит, а если беззнаковое – расширение нулями.
Для операций типа STORE
формат представления чисел не важен, для них core_size_i
сможет принимать значение только от 0 до 2.
Выходной сигнал core_stall_o
нужен для остановки программного счетчика. Ранее логика этого сигнала временно находилась в модуле riscv_unit
— теперь она займет свое законное место в модуле LSU.
Интерфейс блока загрузки/сохранения и памяти
В параграфе описывается организация внешней памяти, и то, как к ней подключается LSU.
Память данных имеет 32-битную разрядность ячейки памяти и поддерживает побайтовую адресацию. Это значит, что существует возможность записи значения по одному байту в пределах одного слова (4-байтовой ячейки памяти). Для указания на необходимые байты интерфейс к памяти предусматривает использование 4-битного сигнала mem_be_o
, подаваемого вместе с адресом слова mem_addr_o
. Позиции битов 4-битного сигнала соответствуют позициям байтов в слове. Если конкретный бит mem_be_o
равен 1, то соответствующий ему байт будет записан в память. Данные для записи подаются на выход mem_wd_o
. На результат чтения из памяти состояние mem_be_o
не влияет, так как чтение производится всегда по 32-бита.
После получения запроса на чтение/запись из ядра, LSU перенаправляет запрос в память данных, взаимодействие осуществляется следующими сигналами:
- сигнал
mem_req_o
сообщает памяти о наличии запроса в память (напрямую подключен кcore_req_i
); - сигнал
mem_we_o
сообщает памяти о типе этого запроса (напрямую подключен кcore_we_i
):mem_we_o
равен 1, если отправлен запрос на запись,mem_we_o
равен 0, если отправлен запрос на чтение;
- сигнал
mem_wd_o
содержит данные на запись в память. В зависимости от размера записи, данные этого сигнала будут отличаться от пришедшего сигналаcore_wd_i
и будут является результатом определенных преобразований. - сигнал
mem_rd_i
содержит считанные из памяти данные. Перед тем, как вернуть считанные данные ядру через выходной сигналcore_rd_o
, эти данные будет необходимо подготовить. - сигнал
mem_ready_i
сообщает о готовности памяти завершить транзакцию на текущем такте. Этот сигнал используется для управления выходным сигналомcore_stall_o
.
Практика
Познай как описать выходные сигналы модуля — и ты познаешь как описать сам модуль. ©Джейсон Стейтем
Реализация любого модуля сводится к реализации логики, управляющей каждым отдельным выходным сигналом посредством входных сигналов. Разберем принцип работы каждого выходного сигнала:
mem_req_o, mem_we_o, mem_addr_o
Все эти сигналы подключаются напрямую к соответствующим core-сигналам:
mem_req_o
кcore_req_i
;mem_we_o
кcore_we_i
;mem_addr_o
кcore_addr_i
.
mem_be_o
По запросу на запись (core_req_i == 1
, core_we_i == 1
), если core_size_i
соответствует инструкции записи байта (SB
), то в сигнале mem_be_o
бит с индексом равным значению двух младших бит адреса core_addr_i
должен быть равен единице.
Допустим, пришел запрос на запись байта по адресу 18:
core_req_i == 1
,core_we_i == 1
,core_size_i == LDST_B
core_addr_i == 32'b10010
В данном случае, необходимо выставить единицу во втором (считая с нуля) бите сигнала mem_be_o
(поскольку значение двух младших бит core_addr_i
равно двум): mem_be_o == 4'b0100
.
Если пришел запрос на запись полуслова (SH
, core_size_i == LDST_H
), то в сигнале mem_be_o
необходимо выставить в единицу либо два старших, либо два младших бита (в зависимости от core_addr[1]
)
Если пришел запрос на запись слова (SW
, core_size_i == LDST_W
), то в сигнале mem_be_o
необходимо выставить в единицу все биты.
mem_wd_o
Сигнал mem_wd_o
функционально связан с сигналом mem_be_o
, т.к. они оба выполняют функцию записи конкретных байт в памяти. Допустим процессор хочет записать байт 0xA5
по адресу 18. Для этого он формирует сигналы:
core_req_i == 1
,core_we_i == 1
,core_size_i == LDST_B
core_addr_i == 32'b10010
core_wd_i == 32h0000_00A5
Мы уже знаем, что mem_be_o
должен быть при этом равен 4'b0100
. Однако если в память придут сигналы:
mem_be_o == 4'b0100
,mem_wd_o == 32'h0000_00A5
то по адресу 18 будет записано значение 0x00
(поскольку второй байт на шине mem_wd_o
равен нулю).
Для того, чтобы по 18-ому адресу записалось значение A5
, это значение должно оказаться во втором байте mem_wd_o
. А в случае 17-го адреса, значение должно оказаться в первом байте и т.п.
Получается, что в случае записи байта, проще всего продублировать записываемый байт во все байты шины mem_wd_o
, ведь в память запишется только тот, которому будет соответствовать бит mem_be_o
, равный единице. Дублирование можно осуществить с помощью конкатенации.
В случае записи полуслова (core_size_i == LDST_H
) ситуация схожа, только теперь дублировать надо не 1 байт 4 раза, а полслова (16 младших бит шины core_wd_i
) два раза.
В случае записи слова (core_size_i == LDST_W
), сигнал mem_wd_o
будет повторять сигнал core_wd_i
.
core_rd_o
Сигнал core_rd_o
— это сигнал, который будет содержать данные для записи в регистровый файл процессора во время инструкций загрузки из памяти (LW
, LH
, LHU
, LB
, LBU
). Чтобы понять, как управлять этим сигналом, нужно понять, что происходит во время этих инструкций.
Предположим, по адресам 16-19
лежит слово 32'hA55A_1881
. Чтение по любому из адресов 16, 17, 18, 19 вернет это слово на входном сигнале mem_rd_i
. В случае инструкции LB
(core_size_i == LDST_B
) по адресу 19 (чтение байта, который интерпретируется как знаковое число), в регистровый файл должно быть записано значение 32'hFFFF_FFA5
, поскольку по 19-ому адресу лежит байт A5
, который затем будет знакорасширен. В случае той же самой инструкции, но по адресу 18, в регистровый файл будет записано значение 32'h0000_005A
(знакорасширенный байт 5A
, расположенный по 18ому адресу).
Получить нужный байт можно из входного сигнала mem_rd_i
, но чтобы понять какие биты этого сигнала нас интересуют, необходимо посмотреть на входные сигналы core_size_i
и core_addr_i[1:0]
. core_size_i
сообщит конкретный тип инструкции (сколько нужно взять байт из считанного слова), а core_addr_i[1:0]
укажет номер начального байта, который нужно взять из mem_rd_i
.
В случае инструкции LH
будет все тоже самое, только знакорасширяться будет не байт, а полуслово.
А для инструкций LBU
и LHU
будет все тоже самое, только результат будет не знакорасширен, а дополнен нулями.
Для инструкций LW
на выход core_rd_o
пойдут данные mem_rd_i
без изменений.
core_stall_o
Сигнал core_stall_o
запрещает менять значение программного счетчика на время обращения в память. Этот сигнал должен:
- стать равным единице в тот же такт, когда пришел сигнал
core_req_i
- удерживать это значение до тех пор, пока не придет сигнал
mem_ready_i
, но не менее 1 такта (т.е. даже если сигналmem_ready_i
будет равен единице,core_req_i
должен подняться хотя бы на 1 такт).
Для реализации подобного функционала вам потребуется вспомогательный регистр stall_reg
, каждый такт записывающий значение выхода core_stall_o
и таблица истинности для этого выхода, представленная на рис. 2.
Рисунок 2. Таблица истинности выхода core_stall_o
.
Задание
Реализовать блок загрузки и сохранения со следующим прототипом:
module riscv_lsu(
input logic clk_i,
input logic rst_i,
// Интерфейс с ядром
input logic core_req_i,
input logic core_we_i,
input logic [ 2:0] core_size_i,
input logic [31:0] core_addr_i,
input logic [31:0] core_wd_i,
output logic [31:0] core_rd_o,
output logic core_stall_o,
// Интерфейс с памятью
output logic mem_req_o,
output logic mem_we_o,
output logic [ 3:0] mem_be_o,
output logic [31:0] mem_addr_o,
output logic [31:0] mem_wd_o,
input logic [31:0] mem_rd_i,
input logic mem_ready_i
);
Рисунок 3. Структурная схема модуля riscv_lsu
.
Порядок выполнения задания
- Внимательно ознакомьтесь с описанием функционального поведения выходов LSU. В случае возникновения вопросов, проконсультируйтесь с преподавателем.
- Реализуйте модуль
riscv_lsu
. Для этого:- В
Design Sources
проекта с предыдущих лаб, создайтеSystemVerilog
-файлriscv_lsu.sv
. - Опишите в нем модуль
riscv_lsu
с таким же именем и портами, как указано в задании.- При описании обратите внимание на то, что большая часть модуля является чисто комбинационной. В этом плане реализация модуля будет частично похожа на реализацию декодера.
- Однако помимо комбинационной части, в модуле будет присутствовать и один регистр.
- В
- После описания модуля, его необходимо проверить с помощью тестового окружения.
- Тестовое окружение находится здесь.
- Для запуска симуляции воспользуйтесь
этой инструкцией
. - Перед запуском симуляции убедитесь, что в качестве top-level модуля выбран корректный (
tb_lsu
). - Во время симуляции, вы должны прожать "Run All" и убедиться, что в логе есть сообщение о завершении теста!